All files / api user_data_converter.ts

95.49% Statements 233/244
89.38% Branches 101/113
100% Functions 37/37
95.34% Lines 225/236
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667                                  2x   2x 2x                       2x                   2x 2x 2x 2x 2x 2x   2x 2x   2x 2x       2x         2x   2x     2x   448x 448x 448x     2x 448x 448x 14x       434x   448x 10x   448x   2x     2x   112x 112x 112x     2x 72x     72x 36x   72x   2x             2x 2x 2x 2x 2x       6777x       1334x   5443x             2x                                                   13312x 13312x 13312x 13312x           13312x 6536x   13312x 13312x 13312x     2x 6425x 6425x               6425x 6401x     2x 243x 243x               243x 219x     2x     108x                   2x   262x     262x               2x     6779x     6779x 278x       2x 6703x 48x     2x                                         2x 22x 2x           2x 433x     2x 560x         560x   512x   434x               2x 14x         14x   14x 14x 14x               286x 286x         286x   240x 240x 240x 271x   211x 187x 181x   5x   176x 116x 90x 90x         90x 90x       2x           22x         22x 22x   22x               22x 10x         10x     22x 22x   22x 32x 32x 32x 32x       32x 32x 22x 22x         22x 22x             2x 5654x         5654x 5654x 5654x       5654x       2x 13116x 13116x   12x 12x                         2x 12897x 12891x     84x 12x       72x 70x   72x 12807x 6094x 6058x       6713x 6631x   6713x       2x 72x 72x 108x 108x       72x         72x 72x   36x     6058x 6058x 6058x 6425x       6305x 6263x     5938x                 2x       6713x 20x 6693x 4783x 1344x   3439x   1910x 134x 1776x 1572x 204x 18x 186x 17x 169x 9x 160x 22x 138x 90x 16x 4x 12x 6x         6x           6x       74x 74x           74x           74x         74x         48x         2x                   19761x                                 6954x 130x 130x       130x               2x       320x 58x 262x 262x                                         533x 533x   120x 120x                       132x    
/**
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
import { DatabaseId } from '../core/database_info';
import { Timestamp } from '../core/timestamp';
import { DocumentKey } from '../model/document_key';
import { FieldValue, ObjectValue } from '../model/field_value';
import {
  ArrayValue,
  BlobValue,
  BooleanValue,
  DoubleValue,
  GeoPointValue,
  IntegerValue,
  NullValue,
  RefValue,
  StringValue,
  TimestampValue
} from '../model/field_value';
import {
  FieldMask,
  FieldTransform,
  Mutation,
  PatchMutation,
  Precondition,
  ServerTimestampTransform,
  SetMutation,
  TransformMutation
} from '../model/mutation';
import { FieldPath } from '../model/path';
import { assert, fail } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { isPlainObject, valueDescription } from '../util/input_validation';
import { AnyJs, primitiveComparator } from '../util/misc';
import * as objUtils from '../util/obj';
import { Dict } from '../util/obj';
import { SortedMap } from '../util/sorted_map';
import * as typeUtils from '../util/types';
 
import { Blob } from './blob';
import {
  FieldPath as ExternalFieldPath,
  fromDotSeparatedString
} from './field_path';
import {
  DeleteFieldValueImpl,
  FieldValueImpl,
  ServerTimestampFieldValueImpl
} from './field_value';
import { GeoPoint } from './geo_point';
 
const RESERVED_FIELD_REGEX = /^__.*__$/;
 
/** The result of parsing document data (e.g. for a setData call). */
export class ParsedSetData {
  constructor(
    readonly data: ObjectValue,
    readonly fieldMask: FieldMask | null,
    readonly fieldTransforms: FieldTransform[]
  ) {}
 
  toMutations(key: DocumentKey, precondition: Precondition): Mutation[] {
    const mutations = [] as Mutation[];
    if (this.fieldMask !== null) {
      mutations.push(
        new PatchMutation(key, this.data, this.fieldMask, precondition)
      );
    } else {
      mutations.push(new SetMutation(key, this.data, precondition));
    }
    if (this.fieldTransforms.length > 0) {
      mutations.push(new TransformMutation(key, this.fieldTransforms));
    }
    return mutations;
  }
}
 
/** The result of parsing "update" data (i.e. for an updateData call). */
export class ParsedUpdateData {
  constructor(
    readonly data: ObjectValue,
    readonly fieldMask: FieldMask,
    readonly fieldTransforms: FieldTransform[]
  ) {}
 
  toMutations(key: DocumentKey, precondition: Precondition): Mutation[] {
    const mutations = [
      new PatchMutation(key, this.data, this.fieldMask, precondition)
    ] as Mutation[];
    if (this.fieldTransforms.length > 0) {
      mutations.push(new TransformMutation(key, this.fieldTransforms));
    }
    return mutations;
  }
}
 
/*
 * Represents what type of API method provided the data being parsed; useful
 * for determining which error conditions apply during parsing and providing
 * better error messages.
 */
enum UserDataSource {
  Set,
  Update,
  MergeSet,
  QueryValue // from a where clause or cursor bound
}
 
function isWrite(dataSource: UserDataSource) {
  switch (dataSource) {
    case UserDataSource.Set: // fall through
    case UserDataSource.MergeSet: // fall through
    case UserDataSource.Update:
      return true;
    case UserDataSource.QueryValue:
      return false;
    default:
      throw fail(`Unexpected case for UserDataSource: ${dataSource}`);
  }
}
 
/** A "context" object passed around while parsing user data. */
class ParseContext {
  readonly fieldTransforms: FieldTransform[];
  readonly fieldMask: FieldPath[];
  /**
   * Initializes a ParseContext with the given source and path.
   *
   * @param dataSource Indicates what kind of API method this data came from.
   * @param methodName The name of the method the user called to create this
   *     ParseContext.
   * @param path A path within the object being parsed. This could be an empty
   *     path (in which case the context represents the root of the data being
   *     parsed), or a nonempty path (indicating the context represents a nested
   *     location within the data).
   * @param arrayElement Whether or not this context corresponds to an element
   *     of an array.
   * @param fieldTransforms A mutable list of field transforms encountered while
   *     parsing the data.
   * @param fieldMask A mutable list of field paths encountered while parsing
   *     the data.
   *
   * TODO(b/34871131): We don't support array paths right now, so path can be
   * null to indicate the context represents any location within an array (in
   * which case certain features will not work and errors will be somewhat
   * compromised).
   */
  constructor(
    readonly dataSource: UserDataSource,
    readonly methodName: string,
    readonly path: FieldPath | null,
    readonly arrayElement?: boolean,
    fieldTransforms?: FieldTransform[],
    fieldMask?: FieldPath[]
  ) {
    // Minor hack: If fieldTransforms is undefined, we assume this is an
    // external call and we need to validate the entire path.
    if (fieldTransforms === undefined) {
      this.validatePath();
    }
    this.arrayElement = arrayElement !== undefined ? arrayElement : false;
    this.fieldTransforms = fieldTransforms || [];
    this.fieldMask = fieldMask || [];
  }
 
  childContextForField(field: string): ParseContext {
    const childPath = this.path == null ? null : this.path.child(field);
    const context = new ParseContext(
      this.dataSource,
      this.methodName,
      childPath,
      /*arrayElement=*/ false,
      this.fieldTransforms,
      this.fieldMask
    );
    context.validatePathSegment(field);
    return context;
  }
 
  childContextForFieldPath(field: FieldPath): ParseContext {
    const childPath = this.path == null ? null : this.path.child(field);
    const context = new ParseContext(
      this.dataSource,
      this.methodName,
      childPath,
      /*arrayElement=*/ false,
      this.fieldTransforms,
      this.fieldMask
    );
    context.validatePath();
    return context;
  }
 
  childContextForArray(index: number): ParseContext {
    // TODO(b/34871131): We don't support array paths right now; so make path
    // null.
    return new ParseContext(
      this.dataSource,
      this.methodName,
      /*path=*/ null,
      /*arrayElement=*/ true,
      this.fieldTransforms,
      this.fieldMask
    );
  }
 
  createError(reason: string): Error {
    const fieldDescription =
      this.path === null || this.path.isEmpty()
        ? ''
        : ` (found in field ${this.path.toString()})`;
    return new FirestoreError(
      Code.INVALID_ARGUMENT,
      `Function ${this.methodName}() called with invalid data. ` +
        reason +
        fieldDescription
    );
  }
 
  private validatePath() {
    // TODO(b/34871131): Remove null check once we have proper paths for fields
    // within arrays.
    Iif (this.path === null) {
      return;
    }
    for (let i = 0; i < this.path.length; i++) {
      this.validatePathSegment(this.path.get(i));
    }
  }
 
  private validatePathSegment(segment: string) {
    if (isWrite(this.dataSource) && RESERVED_FIELD_REGEX.test(segment)) {
      throw this.createError('Document fields cannot begin and end with __');
    }
  }
}
/**
 * An interface that allows arbitrary pre-converting of user data. This
 * abstraction allows for, e.g.:
 *  * The public API to convert DocumentReference objects to DocRef objects,
 *    avoiding a circular dependency between user_data_converter.ts and
 *    database.ts
 *  * Tests to convert test-only sentinels (e.g. '<DELETE>') into types
 *    compatible with UserDataConverter.
 *
 * Returns the converted value (can return back the input to act as a no-op).
 *
 * It can also throw an Error which will be wrapped into a friendly message.
 */
export type DataPreConverter = (input: AnyJs) => AnyJs;
 
/**
 * A placeholder object for DocumentReferences in this file, in order to
 * avoid a circular dependency. See the comments for `DataPreConverter` for
 * the full context.
 */
export class DocumentKeyReference {
  constructor(public databaseId: DatabaseId, public key: DocumentKey) {}
}
 
/**
 * Helper for parsing raw user input (provided via the API) into internal model
 * classes.
 */
export class UserDataConverter {
  constructor(private preConverter: DataPreConverter) {}
 
  /** Parse document data from a non-merge set() call. */
  parseSetData(methodName: string, input: AnyJs): ParsedSetData {
    const context = new ParseContext(
      UserDataSource.Set,
      methodName,
      FieldPath.EMPTY_PATH
    );
    validatePlainObject('Data must be an object, but it was:', context, input);
 
    const updateData = this.parseData(input, context);
 
    return new ParsedSetData(
      updateData as ObjectValue,
      /* fieldMask= */ null,
      context.fieldTransforms
    );
  }
 
  /** Parse document data from a set() call with '{merge:true}'. */
  parseMergeData(methodName: string, input: AnyJs): ParsedSetData {
    const context = new ParseContext(
      UserDataSource.MergeSet,
      methodName,
      FieldPath.EMPTY_PATH
    );
    validatePlainObject('Data must be an object, but it was:', context, input);
 
    const updateData = this.parseData(input, context);
    const fieldMask = new FieldMask(context.fieldMask);
    return new ParsedSetData(
      updateData as ObjectValue,
      fieldMask,
      context.fieldTransforms
    );
  }
 
  /** Parse update data from an update() call. */
  parseUpdateData(methodName: string, input: AnyJs): ParsedUpdateData {
    const context = new ParseContext(
      UserDataSource.Update,
      methodName,
      FieldPath.EMPTY_PATH
    );
    validatePlainObject('Data must be an object, but it was:', context, input);
 
    const fieldMaskPaths = [] as FieldPath[];
    let updateData = ObjectValue.EMPTY;
    objUtils.forEach(input as Dict<AnyJs>, (key, value) => {
      const path = fieldPathFromDotSeparatedString(methodName, key);
 
      const childContext = context.childContextForFieldPath(path);
      value = this.runPreConverter(value, childContext);
      if (value instanceof DeleteFieldValueImpl) {
        // Add it to the field mask, but don't add anything to updateData.
        fieldMaskPaths.push(path);
      } else {
        const parsedValue = this.parseData(value, childContext);
        if (parsedValue != null) {
          fieldMaskPaths.push(path);
          updateData = updateData.set(path, parsedValue);
        }
      }
    });
 
    const mask = new FieldMask(fieldMaskPaths);
    return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
  }
 
  /** Parse update data from a list of field/value arguments. */
  parseUpdateVarargs(
    methodName: string,
    field: string | ExternalFieldPath,
    value: AnyJs,
    moreFieldsAndValues: AnyJs[]
  ): ParsedUpdateData {
    const context = new ParseContext(
      UserDataSource.Update,
      methodName,
      FieldPath.EMPTY_PATH
    );
    const keys = [fieldPathFromArgument(methodName, field)];
    const values = [value];
 
    Iif (moreFieldsAndValues.length % 2 !== 0) {
      throw new FirestoreError(
        Code.INVALID_ARGUMENT,
        `Function ${methodName}() needs to be called with an even number ` +
          'of arguments that alternate between field names and values.'
      );
    }
 
    for (let i = 0; i < moreFieldsAndValues.length; i += 2) {
      keys.push(
        fieldPathFromArgument(methodName, moreFieldsAndValues[i] as
          | string
          | ExternalFieldPath)
      );
      values.push(moreFieldsAndValues[i + 1]);
    }
 
    const fieldMaskPaths = [] as FieldPath[];
    let updateData = ObjectValue.EMPTY;
 
    for (let i = 0; i < keys.length; ++i) {
      const path = keys[i];
      const childContext = context.childContextForFieldPath(path);
      const value = this.runPreConverter(values[i], childContext);
      Iif (value instanceof DeleteFieldValueImpl) {
        // Add it to the field mask, but don't add anything to updateData.
        fieldMaskPaths.push(path);
      } else {
        const parsedValue = this.parseData(value, childContext);
        if (parsedValue != null) {
          fieldMaskPaths.push(path);
          updateData = updateData.set(path, parsedValue);
        }
      }
    }
 
    const mask = new FieldMask(fieldMaskPaths);
    return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
  }
 
  /**
   * Parse a "query value" (e.g. value in a where filter or a value in a cursor
   * bound).
   */
  parseQueryValue(methodName: string, input: AnyJs): FieldValue {
    const context = new ParseContext(
      UserDataSource.QueryValue,
      methodName,
      FieldPath.EMPTY_PATH
    );
    const parsed = this.parseData(input, context);
    assert(parsed != null, 'Parsed data should not be null.');
    assert(
      context.fieldTransforms.length === 0,
      'Field transforms should have been disallowed.'
    );
    return parsed!;
  }
 
  /** Sends data through this.preConverter, handling any thrown errors. */
  private runPreConverter(input: AnyJs, context: ParseContext): AnyJs {
    try {
      return this.preConverter(input);
    } catch (e) {
      const message = errorMessage(e);
      throw context.createError(message);
    }
  }
 
  /**
   * Internal helper for parsing user data.
   *
   * @param input Data to be parsed.
   * @param context A context object representing the current path being parsed,
   * the source of the data being parsed, etc.
   * @return The parsed value, or null if the value was a FieldValue sentinel
   * that should not be included in the resulting parsed data.
   */
  private parseData(input: AnyJs, context: ParseContext): FieldValue | null {
    input = this.runPreConverter(input, context);
    if (input instanceof Array) {
      // TODO(b/34871131): Include the path containing the array in the error
      // message.
      if (context.arrayElement) {
        throw context.createError('Nested arrays are not supported');
      }
      // If context.path is null we are already inside an array and we don't
      // support field mask paths more granular than the top-level array.
      if (context.path) {
        context.fieldMask.push(context.path);
      }
      return this.parseArray(input as AnyJs[], context);
    } else if (looksLikeJsonObject(input)) {
      validatePlainObject('Unsupported field value:', context, input);
      return this.parseObject(input as Dict<AnyJs>, context);
    } else {
      // If context.path is null, we are inside an array and we should have
      // already added the root of the array to the field mask.
      if (context.path) {
        context.fieldMask.push(context.path);
      }
      return this.parseScalarValue(input, context);
    }
  }
 
  private parseArray(array: AnyJs[], context: ParseContext): FieldValue {
    const result = [] as FieldValue[];
    let entryIndex = 0;
    for (const entry of array) {
      let parsedEntry = this.parseData(
        entry,
        context.childContextForArray(entryIndex)
      );
      Iif (parsedEntry == null) {
        // Just include nulls in the array for fields being replaced with a
        // sentinel.
        parsedEntry = NullValue.INSTANCE;
      }
      result.push(parsedEntry);
      entryIndex++;
    }
    return new ArrayValue(result);
  }
 
  private parseObject(obj: Dict<AnyJs>, context: ParseContext): FieldValue {
    let result = new SortedMap<string, FieldValue>(primitiveComparator);
    objUtils.forEach(obj, (key: string, val: AnyJs) => {
      const parsedValue = this.parseData(
        val,
        context.childContextForField(key)
      );
      if (parsedValue != null) {
        result = result.insert(key, parsedValue);
      }
    });
    return new ObjectValue(result);
  }
 
  /**
   * Helper to parse a scalar value (i.e. not an Object or Array)
   *
   * @return The parsed value, or null if the value was a FieldValue sentinel
   * that should not be included in the resulting parsed data.
   */
  private parseScalarValue(
    value: AnyJs,
    context: ParseContext
  ): FieldValue | null {
    if (value === null) {
      return NullValue.INSTANCE;
    } else if (typeof value === 'number') {
      if (typeUtils.isSafeInteger(value)) {
        return new IntegerValue(value);
      } else {
        return new DoubleValue(value);
      }
    } else if (typeof value === 'boolean') {
      return BooleanValue.of(value);
    } else if (typeof value === 'string') {
      return new StringValue(value);
    } else if (value instanceof Date) {
      return new TimestampValue(Timestamp.fromDate(value));
    } else if (value instanceof GeoPoint) {
      return new GeoPointValue(value);
    } else if (value instanceof Blob) {
      return new BlobValue(value);
    } else if (value instanceof DocumentKeyReference) {
      return new RefValue(value.databaseId, value.key);
    } else if (value instanceof FieldValueImpl) {
      if (value instanceof DeleteFieldValueImpl) {
        if (context.dataSource === UserDataSource.MergeSet) {
          return null;
        } else if (context.dataSource === UserDataSource.Update) {
          assert(
            context.path == null || context.path.length > 0,
            'FieldValue.delete() at the top level should have already' +
              ' been handled.'
          );
          throw context.createError(
            'FieldValue.delete() can only appear at the top level ' +
              'of your update data'
          );
        } else {
          // We shouldn't encounter delete sentinels for queries or non-merge set() calls.
          throw context.createError(
            'FieldValue.delete() can only be used with update() and set() with {merge:true}'
          );
        }
      } else Eif (value instanceof ServerTimestampFieldValueImpl) {
        Iif (!isWrite(context.dataSource)) {
          throw context.createError(
            'FieldValue.serverTimestamp() can only be used with set()' +
              ' and update()'
          );
        }
        Iif (context.path === null) {
          throw context.createError(
            'FieldValue.serverTimestamp() is not currently' +
              ' supported inside arrays'
          );
        }
        context.fieldTransforms.push(
          new FieldTransform(context.path, ServerTimestampTransform.instance)
        );
 
        // Return null so this value is omitted from the parsed result.
        return null;
      } else {
        return fail('Unknown FieldValue type: ' + value);
      }
    } else {
      throw context.createError(
        `Unsupported field value: ${valueDescription(value)}`
      );
    }
  }
}
 
/**
 * Checks whether an object looks like a JSON object that should be converted
 * into a struct. Normal class/prototype instances are considered to look like
 * JSON objects since they should be converted to a struct value. Arrays, Dates,
 * GeoPoints, etc. are not considered to look like JSON objects since they map
 * to specific FieldValue types other than ObjectValue.
 */
function looksLikeJsonObject(input: AnyJs): boolean {
  return (
    typeof input === 'object' &&
    input !== null &&
    !(input instanceof Array) &&
    !(input instanceof Date) &&
    !(input instanceof GeoPoint) &&
    !(input instanceof Blob) &&
    !(input instanceof DocumentKeyReference) &&
    !(input instanceof FieldValueImpl)
  );
}
 
function validatePlainObject(
  message: string,
  context: ParseContext,
  input: AnyJs
) {
  if (!looksLikeJsonObject(input) || !isPlainObject(input)) {
    const description = valueDescription(input);
    Iif (description === 'an object') {
      // Massage the error if it was an object.
      throw context.createError(message + ' a custom object');
    } else {
      throw context.createError(message + ' ' + description);
    }
  }
}
 
/**
 * Helper that calls fromDotSeparatedString() but wraps any error thrown.
 */
export function fieldPathFromArgument(
  methodName: string,
  path: string | ExternalFieldPath
): FieldPath {
  if (path instanceof ExternalFieldPath) {
    return path._internalPath;
  } else Eif (typeof path === 'string') {
    return fieldPathFromDotSeparatedString(methodName, path);
  } else {
    const message = 'Field path arguments must be of type string or FieldPath.';
    throw new FirestoreError(
      Code.INVALID_ARGUMENT,
      `Function ${methodName}() called with invalid data. ${message}`
    );
  }
}
 
/**
 * Wraps fromDotSeparatedString with an error message about the method that
 * was thrown.
 * @param methodName The publicly visible method name
 * @param path The dot-separated string form of a field path which will be split
 * on dots.
 */
function fieldPathFromDotSeparatedString(
  methodName: string,
  path: string
): FieldPath {
  try {
    return fromDotSeparatedString(path)._internalPath;
  } catch (e) {
    const message = errorMessage(e);
    throw new FirestoreError(
      Code.INVALID_ARGUMENT,
      `Function ${methodName}() called with invalid data. ${message}`
    );
  }
}
 
/**
 * Extracts the message from a caught exception, which should be an Error object
 * though JS doesn't guarantee that.
 */
function errorMessage(error: Error | object): string {
  return error instanceof Error ? error.message : error.toString();
}